package org.unidata.mdm.draft.dao.impl;

import java.sql.Array;
import java.sql.Types;
import java.util.List;
import java.util.Properties;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.unidata.mdm.draft.dao.DraftsDAO;
import org.unidata.mdm.draft.po.DraftPO;
import org.unidata.mdm.draft.po.EditionPO;
import org.unidata.mdm.draft.type.DraftPublicationOrder;
import org.unidata.mdm.system.dao.impl.BaseDAOImpl;
import org.unidata.mdm.system.serialization.SystemSerializer;
import org.unidata.mdm.system.type.runtime.MeasurementPoint;

/**
 * @author Mikhail Mikhailov on Sep 9, 2020
 */
@Repository
public class DraftsDAOImpl extends BaseDAOImpl implements DraftsDAO {
    /**
     * Default RM for draft objects.
     */
    private static final RowMapper<DraftPO> DEFAULT_DRAFT_ROW_MAPPER = (rs, row) -> {

        DraftPO result = new DraftPO();

        long v = rs.getLong(DraftPO.FIELD_ID);
        result.setId(v == 0 ? null : v);

        v = rs.getLong(DraftPO.FIELD_PARENT_ID);
        result.setParentId(v == 0 ? null : v);

        result.setProvider(rs.getString(DraftPO.FIELD_PROVIDER));
        result.setSubject(rs.getString(DraftPO.FIELD_SUBJECT));
        result.setOwner(rs.getString(DraftPO.FIELD_OWNER));
        result.setDescription(rs.getString(DraftPO.FIELD_DESCRIPTION));

        Array tags = rs.getArray(DraftPO.FIELD_TAGS);
        result.setTags(rs.wasNull() ? null : (String[]) tags.getArray());

        result.setVariables(SystemSerializer.variablesFromProtostuff(rs.getBytes(DraftPO.FIELD_VARIABLES)));

        result.setCreateDate(rs.getTimestamp(DraftPO.FIELD_CREATE_DATE));
        result.setCreatedBy(rs.getString(DraftPO.FIELD_CREATED_BY));
        result.setUpdateDate(rs.getTimestamp(DraftPO.FIELD_UPDATE_DATE));
        result.setUpdatedBy(rs.getString(DraftPO.FIELD_UPDATED_BY));
        result.setEditionsCount(rs.getInt(DraftPO.FIELD_EDITIONS_COUNT));
        result.setPublicationOrder(DraftPublicationOrder.fromString(rs.getString(DraftPO.FIELD_PUBLICATION_ORDER)));

        return result;
    };
    /**
     * Default RM for edition objects.
     */
    private static final RowMapper<EditionPO> DEFAULT_EDITION_ROW_MAPPER = (rs, row) -> {

        EditionPO result = new EditionPO();

        result.setCreateDate(rs.getTimestamp(EditionPO.FIELD_CREATE_DATE));
        result.setCreatedBy(rs.getString(EditionPO.FIELD_CREATED_BY));
        result.setDraftId(rs.getLong(EditionPO.FIELD_DRAFT_ID));
        result.setProvider(rs.getString(EditionPO.FIELD_PROVIDER));
        result.setRevision(rs.getInt(EditionPO.FIELD_REVISION));
        result.setContent(rs.getBytes(EditionPO.FIELD_CONTENT));

        return result;
    };
    /**
     * Drafts filter SQL types.
     */
    private static final int[] DRAFTS_FILTER_TYPES = {
         // Parent id
        Types.BIGINT,
        Types.BIGINT,
        // Provider type (ID)
        Types.VARCHAR,
        Types.VARCHAR,
        // Subject ID
        Types.VARCHAR,
        Types.VARCHAR,
        // Owner
        Types.VARCHAR,
        Types.VARCHAR,
        // Tags
        Types.ARRAY,
        Types.ARRAY,
    };
    /**
     * Drafts filter SQL types.
     */
    private static final int[] DRAFTS_QUERY_TYPES = {
        // Parent id
        Types.BIGINT,
        Types.BIGINT,
        // Provider type (ID)
        Types.VARCHAR,
        Types.VARCHAR,
        // Subject ID
        Types.VARCHAR,
        Types.VARCHAR,
        // Owner
        Types.VARCHAR,
        Types.VARCHAR,
        // Tags
        Types.ARRAY,
        Types.ARRAY,
        // limit, offset
        Types.INTEGER,
        Types.INTEGER
    };
    /**
     * Drafts insert SQL types (needed due to array member).
     */
    private static final int[] DRAFTS_INSERT_TYPES = {
        // Parent id
        Types.BIGINT,
        // Provider type
        Types.VARCHAR,
        // Subject
        Types.VARCHAR,
        // Owner
        Types.VARCHAR,
        // Description
        Types.VARCHAR,
        // Tags
        Types.ARRAY,
        // Created by
        Types.VARCHAR,
        // Publication order
        Types.VARCHAR,
        // Variables
        Types.VARBINARY
    };

    private final String countDraftsSQL;
    private final String loadDraftsSQL;
    private final String loadDraftByIdSQL;
    private final String loadEditionsByDraftIdSQL;
    private final String loadCurrentEditionByDraftIdSQL;
    private final String putEditionSQL;
    private final String putDraftSQL;
    private final String putDraftPropertiesByIdSQL;
    private final String putDraftSubjectByIdSQL;
    private final String deleteDraftByIdSQL;
    private final String deleteDraftsSQL;

    /**
     * Constructor.
     */
    @Autowired
    public DraftsDAOImpl(
            @Qualifier("draftDataSource") final DataSource dataSource,
            @Qualifier("drafts-sql") final Properties sql) {
        super(dataSource);
        countDraftsSQL = sql.getProperty("countDraftsSQL");
        loadDraftsSQL = sql.getProperty("loadDraftsSQL");
        loadDraftByIdSQL = sql.getProperty("loadDraftByIdSQL");
        loadEditionsByDraftIdSQL = sql.getProperty("loadEditionsByDraftIdSQL");
        loadCurrentEditionByDraftIdSQL = sql.getProperty("loadCurrentEditionByDraftIdSQL");
        putEditionSQL = sql.getProperty("putEditionSQL");
        putDraftSQL = sql.getProperty("putDraftSQL");
        putDraftPropertiesByIdSQL = sql.getProperty("putDraftPropertiesByIdSQL");
        putDraftSubjectByIdSQL = sql.getProperty("putDraftSubjectByIdSQL");
        deleteDraftByIdSQL = sql.getProperty("deleteDraftByIdSQL");
        deleteDraftsSQL = sql.getProperty("deleteDraftsSQL");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long countDrafts(Long parentDraftId, String providerId, String subjectId, String owner, String[] tags) {
        MeasurementPoint.start();
        try {

            final Object[] draftsFilter = new Object[] {
                parentDraftId, parentDraftId,
                providerId, providerId,
                subjectId, subjectId,
                owner, owner,
                tags, tags
            };

            return jdbcTemplate.queryForObject(countDraftsSQL, draftsFilter, DRAFTS_FILTER_TYPES, Long.class);
        } finally {
            MeasurementPoint.stop();
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public List<DraftPO> loadDrafts(Long parentDraftId, String providerId, String subjectId, String owner, String[] tags,
            Integer limit, Integer offset) {

        MeasurementPoint.start();
        try {

            final Object[] draftsFilter = new Object[] {
                parentDraftId, parentDraftId,
                providerId, providerId,
                subjectId, subjectId,
                owner, owner,
                tags, tags,
                limit,
                offset
            };

            return jdbcTemplate.query(loadDraftsSQL, draftsFilter, DRAFTS_QUERY_TYPES, DEFAULT_DRAFT_ROW_MAPPER);
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DraftPO loadDraft(long id) {
        MeasurementPoint.start();
        try {
            return jdbcTemplate.query(loadDraftByIdSQL, rs -> rs.next() ? DEFAULT_DRAFT_ROW_MAPPER.mapRow(rs, 0) : null, id);
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public EditionPO loadCurrentEdition(long id, boolean withData) {
        MeasurementPoint.start();
        try {
            return jdbcTemplate.query(loadCurrentEditionByDraftIdSQL, rs -> rs.next() ? DEFAULT_EDITION_ROW_MAPPER.mapRow(rs, 0) : null, withData, id);
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<EditionPO> loadEditions(long id, boolean withData) {
        MeasurementPoint.start();
        try {
            return jdbcTemplate.query(loadEditionsByDraftIdSQL, DEFAULT_EDITION_ROW_MAPPER, withData, id);
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long putDraft(DraftPO draft) {
        MeasurementPoint.start();
        try {

            final Object[] args = new Object[] {
                    draft.getParentId(),
                    draft.getProvider(),
                    draft.getSubject(),
                    draft.getOwner(),
                    draft.getDescription(),
                    draft.getTags(),
                    draft.getCreatedBy(),
                    draft.getPublicationOrder() != null ? draft.getPublicationOrder().name() : null,
                    draft.getVariables() != null && !draft.getVariables().isEmpty()
                        ? SystemSerializer.variablesToProtostuff(draft.getVariables())
                        : null
            };

            return jdbcTemplate.queryForObject(putDraftSQL, args, DRAFTS_INSERT_TYPES, Long.class);
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int putEdition(EditionPO subject) {
        MeasurementPoint.start();
        try {
            return jdbcTemplate.queryForObject(putEditionSQL, Integer.class,
                    subject.getDraftId(),
                    subject.getDraftId(),
                    subject.getContent(),
                    subject.getCreatedBy());
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void putDraftProperties(DraftPO draft) {
        MeasurementPoint.start();
        try {
            jdbcTemplate.update(putDraftPropertiesByIdSQL,
                    draft.getVariables() != null && !draft.getVariables().isEmpty()
                        ? SystemSerializer.variablesToProtostuff(draft.getVariables())
                        : null,
                    draft.getDescription(),
                    draft.getSubject(),
                    draft.getTags(),
                    draft.getPublicationOrder() != null ? draft.getPublicationOrder().name() : null,
                    draft.getId());
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void putSubject(long draftId, String subjectId) {
        MeasurementPoint.start();
        try {
            jdbcTemplate.update(putDraftSubjectByIdSQL, subjectId, draftId);
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int wipeDraft(long draftId) {
        MeasurementPoint.start();
        try {
            return jdbcTemplate.update(deleteDraftByIdSQL, draftId);
        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int wipeDrafts(Long parentDraftId, String providerId, String subjectId, String owner, String[] tags) {
        MeasurementPoint.start();
        try {

            final Object[] draftsFilter = new Object[] {
                parentDraftId, parentDraftId,
                providerId, providerId,
                subjectId, subjectId,
                owner, owner,
                tags, tags
            };

            return jdbcTemplate.update(deleteDraftsSQL, draftsFilter, DRAFTS_FILTER_TYPES);
        } finally {
            MeasurementPoint.stop();
        }
    }
}
